package main

import (
	"encoding/json"
	"flag"
	"fmt"
	"os"
	"os/exec"
	"runtime"
	"time"

	"mariadb-operator/pkg/apis/finupgroup/v1alpha1"
	"mariadb-operator/pkg/constants"
	"mariadb-operator/pkg/storage/qiniu"

	"k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/client-go/util/retry"

	"github.com/operator-framework/operator-sdk/pkg/sdk"
	"github.com/sirupsen/logrus"
	"github.com/streadway/amqp"
)

func printVersion() {
	flag.Parse()
	logrus.Infof("Go Version: %s", runtime.Version())
	logrus.Infof("Go OS/Arch: %s/%s", runtime.GOOS, runtime.GOARCH)
}

func failOnError(err error, msg string) {
	if err != nil {
		panic(fmt.Sprintf("%s: %s", msg, err))
	}
}

var mq = flag.String("mq", "", "--mq=value")

func main() {
	defer func() {
		if err := recover(); err != nil {
			logrus.Error(err)
		}
	}()

	printVersion()
	if *mq == "" {
		mqRUL := os.Getenv("RABBIT_MQ")
		if mqRUL == "" {
			panic("mq url must be define, you can set the RABBIT_MQ env or -mq mqurl")
		}
		*mq = mqRUL
	}
	conn, err := amqp.Dial(*mq)
	failOnError(err, "Failed to connect to RabbitMQ")
	defer conn.Close()

	ch, err := conn.Channel()
	failOnError(err, "Failed to open a channel")
	defer ch.Close()

	q, err := ch.QueueDeclare(
		constants.BackupTaskQueueName, // name
		false, // durable
		false, // delete when usused
		false, // exclusive
		false, // no-wait
		nil,   // arguments
	)

	failOnError(err, "Failed to declare a queue")

	msgs, err := ch.Consume(
		q.Name, // queue
		"",     // consumer
		true,   // auto-ack
		false,  // exclusive
		false,  // no-local
		false,  // no-wait
		nil,    // args
	)
	failOnError(err, "Failed to register a consumer")

	forever := make(chan bool)

	go func() {
		for {
			select {
			case d := <-msgs:
				logrus.Printf("Received a message: %s", d.Body)
				task := &v1alpha1.BackupTask{}
				if err := json.Unmarshal(d.Body, task); err != nil {
					logrus.Errorf("unmarshal task data to backupTask struct err : %v", err)
					continue
				}
				startTime := metav1.Now()
				// exec backup before
				switch task.Type {
				case "backup":
					backupStatus := &v1alpha1.BackupStatus{
						TimeStarted: startTime,
						Conditions: []v1alpha1.BackupCondition{
							v1alpha1.BackupCondition{
								Type: v1alpha1.BackupRunning,
							},
						},
					}
					err = updateStatus(task, backupStatus, nil)
					if err != nil {
						logrus.Error(err)
						continue
					}
				case "cronBackup":
					// TODO
				}

				err = backup(task.ClusterName, task.Command)
				if err != nil {
					logrus.Error(err)
				}

				// exec backup after
				switch task.Type {
				case "backup":
					backupStatus := &v1alpha1.BackupStatus{
						TimeStarted: startTime,
					}
					if err != nil {
						backupStatus.Conditions = []v1alpha1.BackupCondition{
							v1alpha1.BackupCondition{
								LastTransitionTime: metav1.Now(),
								Type:               v1alpha1.BackupFailed,
								Reason:             err.Error(),
								Message:            err.Error(),
							},
						}
					} else {
						backupStatus.Conditions = []v1alpha1.BackupCondition{
							v1alpha1.BackupCondition{
								LastTransitionTime: metav1.Now(),
								Type:               v1alpha1.BackupComplete,
							},
						}
					}
					backupStatus.TimeCompleted = metav1.Now()
					err = updateStatus(task, backupStatus, nil)
					if err != nil {
						logrus.Error(err)
						continue
					}
				case "cronBackup":
					cronStatus := &v1alpha1.CronStatus{
						LastBackup: metav1.Now(),
					}
					if err != nil {
						cronStatus.Conditions = []v1alpha1.BackupCondition{
							v1alpha1.BackupCondition{
								LastTransitionTime: metav1.Now(),
								Type:               v1alpha1.BackupFailed,
								Reason:             err.Error(),
								Message:            err.Error(),
							},
						}
					} else {
						cronStatus.Conditions = []v1alpha1.BackupCondition{
							v1alpha1.BackupCondition{
								Type:               v1alpha1.BackupComplete,
								LastTransitionTime: metav1.Now(),
							},
						}
					}
					err = updateStatus(task, nil, cronStatus)
					if err != nil {
						logrus.Error(err)
						continue
					}
				}
			}

		}
	}()

	logrus.Printf(" [*] Waiting for backup task message. To exit press CTRL+C")
	<-forever
}

/*
backup steps:
1. backup mariadb data
2. upload backup data to s3 storage
*/
func backup(clusterName, cmd string) error {
	// exec backup cmd
	c := exec.Command("bash", "-c", cmd)
	_, err := c.CombinedOutput()
	if err != nil {
		return fmt.Errorf("exec backup cmd %v err : %v", cmd, err)
	}

	// upload backup data to s3 storage
	key := fmt.Sprintf("%s.%s.sql.gz", clusterName, time.Now().UTC().Format("20060102150405"))
	if err := qiniu.Upload(key, constants.BackupFileName); err != nil {
		return fmt.Errorf("upload backup data failed : %v", err)
	}
	logrus.Infof("upload mariadb cluster %v backup data success", clusterName)
	os.RemoveAll(constants.BackupFileName)
	return nil
}

func updateStatus(task *v1alpha1.BackupTask, backupStatus *v1alpha1.BackupStatus, cronStatus *v1alpha1.CronStatus) error {
	switch task.Type {
	case "backup":
		backup := &v1alpha1.MariaDBBackup{
			TypeMeta:   metav1.TypeMeta{APIVersion: "finupgroup.com/v1alpha1", Kind: "MariaDBBackup"},
			ObjectMeta: metav1.ObjectMeta{Name: task.BackupName, Namespace: task.BackupNamespace},
		}
		err := sdk.Get(backup)
		if err != nil {
			return fmt.Errorf("get mariadb backup failed : %v", err)
		}
		backup.Status = *backupStatus

		retryErr := retry.RetryOnConflict(retry.DefaultRetry, func() error {
			return sdk.Update(backup)
		})

		if retryErr != nil && !errors.IsNotFound(retryErr) {
			return fmt.Errorf("update mariadb backup status failed : %v", err)
		}
	case "cronBackup":
		cronBackup := &v1alpha1.MariaDBCronBackup{
			TypeMeta:   metav1.TypeMeta{APIVersion: "finupgroup.com/v1alpha1", Kind: "MariaDBCronBackup"},
			ObjectMeta: metav1.ObjectMeta{Name: task.BackupName, Namespace: task.BackupNamespace},
		}
		err := sdk.Get(cronBackup)
		if err != nil {
			return fmt.Errorf("get mariadb backup failed : %v", err)
		}
		cronStatus.Counts = cronBackup.Status.Counts + 1
		cronBackup.Status = *cronStatus
		err = sdk.Update(cronBackup)
		if err != nil {
			return fmt.Errorf("update mariadb backup status failed : %v", err)
		}
	}
	return nil
}
